package com.dbw.pdftool;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedList;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.text.TextPosition;

import com.dbw.pdftool.PDFTextExtractor.PDFLine;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;

/**
 * 自动的书签生成器。
 * <p>
 * 只需要指定各级书签的一些示例，本生成器就可以依据这些示例，在PDF文本中自动搜索出相同字体的书签。
 * 
 * 
 * @author dongb
 *
 */
public class AutoBookmarkMaker {

    /**
     * 书签格式
     * 
     * @author dongb
     *
     */
    private static final class BookmarkPattern {

        /**
         * 各级书签格式
         */
        private ArrayList<BookmarkLevelPattern> levelPatternList = new ArrayList<BookmarkLevelPattern>();

        public ArrayList<BookmarkLevelPattern> getLevelPatternList() {
            return levelPatternList;
        }

        public void addLevelPattern(BookmarkLevelPattern levelPattern) {
            levelPatternList.add(levelPattern);
        }

        /**
         * 判断该行是否为书签
         * 
         * @param line
         *            待判断的行
         * @return 若是书签，返回其级别，从0开始；否则，返回-1
         */
        public int match(PDFLine line) {
            for (int i = 0; i < getLevelPatternList().size(); i++) {
                if (getLevelPatternList().get(i).match(line)) {
                    return i;
                }
            }

            return -1;
        }
    }

    /**
     * 某一级书签的格式
     * 
     * @author dongb
     *
     */
    private static final class BookmarkLevelPattern {

        /**
         * 书签格式列表，每一级书签，可以有多种格式。
         */
        private ArrayList<BookmarkItemPattern> itemPatternList = new ArrayList<BookmarkItemPattern>();

        public ArrayList<BookmarkItemPattern> getItemPatternList() {
            return itemPatternList;
        }

        public void addItemPattern(BookmarkItemPattern itemPattern) {
            itemPatternList.add(itemPattern);
        }

        public boolean match(PDFLine line) {
            for (BookmarkItemPattern itemPattern : getItemPatternList()) {
                if (itemPattern.match(line)) {
                    return true;
                }
            }

            return false;
        }
    }

    /**
     * 书签荐的格式
     * 
     * @author dongb
     *
     */
    private static final class BookmarkItemPattern {

        private float textHeight;

        public BookmarkItemPattern(float textHeight) {
            setTextHeight(textHeight);
        }

        public void setTextHeight(float textHeight) {
            this.textHeight = textHeight;
        }

        public float getTextHeight() {
            return textHeight;
        }

        public boolean match(PDFLine line) {
            return Math.abs(getTextHeight() - line.getHeadPosition().getHeightDir()) < 0.01;
        }
    }

    /**
     * 书签格式
     */
    private BookmarkPattern bookmarkPattern = null;

    /**
     * PDF文本提取器
     */
    private PDFTextExtractor pdfTextExtractor = null;

    /**
     * 书签根节点
     */
    private PDOutlineItem bookmarkRoot = null;

    /**
     * 父书签栈
     */
    private LinkedList<PDOutlineItem> parentBookmarkStack = null;

    /**
     * 最后一次添加的书签
     * <p>
     * 若之后添加的是下一级书签，则需要将其添加到<tt>lastBookmark</tt>
     */
    private PDOutlineItem lastBookmark = null;

    /**
     * 加载书签格式
     * 
     * @param inputPDF
     * @param bookmarkDemoFile
     */
    private void loadBookmarkPattern(String inputPDF, String bookmarkDemoFile) throws Exception {

        /**
         * 书签示例列表，ArrayList<String>存放某一级书签的示例，而ArrayList<ArrayList<String>>则存入各级书签的示例
         */
        Gson gson = new Gson();
        JsonReader jsonReader = new JsonReader(new InputStreamReader(new FileInputStream(bookmarkDemoFile), "UTF-8"));
        Type listType = new TypeToken<ArrayList<ArrayList<String>>>() {
        }.getType();
        ArrayList<ArrayList<String>> bookmarkDemoList = gson.fromJson(jsonReader, listType);

        System.out.println("bookmark demo list:" + bookmarkDemoList);

        /* create PDFExtractor */
        pdfTextExtractor = new PDFTextExtractor(new File(inputPDF));
        pdfTextExtractor.extract();

        // System.out.println("========================lines=========================");
        // for(PDFLine line : pdfTextExtractor.getLineList()) {
        // System.out.println(line.getText());
        // }
        // System.out.println("======================================================");

        bookmarkPattern = new BookmarkPattern();
        for (int level = 0; level < bookmarkDemoList.size(); level++) {
            BookmarkLevelPattern levelPattern = new BookmarkLevelPattern();
            for (String itemDemo : bookmarkDemoList.get(level)) {
                if (itemDemo == null) {
                    continue;
                }
                PDFLine bestMatchedLine = findBestMatchedLine(itemDemo);
                if (bestMatchedLine == null) {
                    continue;
                }
                levelPattern.addItemPattern(new BookmarkItemPattern(bestMatchedLine.getHeadPosition().getHeightDir()));
            }
            bookmarkPattern.addLevelPattern(levelPattern);
        }
    }

    private PDFLine findBestMatchedLine(String dstLine) {
        PDFLine bestLine = null;
        for (PDFLine line : pdfTextExtractor.getLineList()) {
            if (line.getText().startsWith(dstLine) && (bestLine == null
                    || bestLine.getHeadPosition().getHeightDir() < line.getHeadPosition().getHeightDir())) {
                bestLine = line;
            }
        }

        if (bestLine != null) {
            System.out.printf("[%f] %s\n", bestLine.getHeadPosition().getHeightDir(), dstLine);
        }
        return bestLine;
    }

    /**
     * 加载书签
     * 
     * @param inputBookmarkFile
     *            文本格式的书签文件路径
     */
    private void loadBookmark(String inputBookmarkFile) throws Exception {

        bookmarkRoot = new PDOutlineItem();
        bookmarkRoot.setTitle("Root");
        parentBookmarkStack = new LinkedList<PDOutlineItem>();
        parentBookmarkStack.push(bookmarkRoot);
        lastBookmark = null;

        try {
            for (PDFLine line : pdfTextExtractor.getLineList()) {

                int level = bookmarkPattern.match(line);
                if (level == -1) {
                    continue;
                }

                System.out.println(level + ": " + line.getText());

                // 创建书签
                PDOutlineItem bookmark = createBookmarkItem(line);
                if (bookmark == null) {
                    System.err.println("Can't create bookmark for: " + line);
                    continue;
                }

                // 将书签添加到书签集中
                addBookmarkItem(bookmark, level + 1);
            }
        } finally {
            // do nothing
        }
    }

    private PDOutlineItem createBookmarkItem(PDFLine line) {

        PDOutlineItem bookmarkItem = new PDOutlineItem();

        // 设置标题
        bookmarkItem.setTitle(line.getText());

        // 设置页数
        PDPageXYZDestination dest = new PDPageXYZDestination();
        dest.setPageNumber(line.getPageNo() - 1);
        TextPosition textPosition = line.getHeadPosition();
        try {
            float pageHeight = 0;
            Field field = TextPosition.class.getDeclaredField("pageHeight");
            field.setAccessible(true);
            pageHeight = field.getFloat(textPosition);
            dest.setTop((int) (pageHeight - textPosition.getY() + textPosition.getHeight() * 1.5));
        } catch (Exception e) {
            e.printStackTrace();
        }
        bookmarkItem.setDestination(dest);

        return bookmarkItem;
    }

    private void addBookmarkItem(PDOutlineItem newItem, int level) throws IOException {

        if (level == parentBookmarkStack.size()) {
            // 级数不变
            parentBookmarkStack.peek().addLast(newItem);
        } else if (level > parentBookmarkStack.size()) {
            // 级数增加
            /* 将上次的书签添加到父书签栈 */
            if (lastBookmark != null) {
                parentBookmarkStack.push(lastBookmark);
            }
            /*
             * 若书签级别只是加1，此时parentBookmarkStack的大小应该与level相等。
             * 若还是不等，说明发生了跳级。先自动补充缺少的书签。
             */
            while (level > parentBookmarkStack.size()) {
                PDOutlineItem blankItem = new PDOutlineItem();
                blankItem.setDestination(newItem.getDestination());
                blankItem.setTitle("blank");
                parentBookmarkStack.peek().addLast(blankItem);
                parentBookmarkStack.push(blankItem);
            }
            parentBookmarkStack.peek().addLast(newItem);
        } else {
            // 级数减小
            while (level < parentBookmarkStack.size()) {
                parentBookmarkStack.pop();
            }
            parentBookmarkStack.peek().addLast(newItem);
        }

        lastBookmark = newItem;
    }

    /**
     * 自动生成书签
     * 
     * @param inputPDF
     *            待生成书签的PDF文件路径
     * @param outputPDF
     *            添加书签后的PDF文件路径
     * 
     * @throws Exception
     */
    private void createBookmark(String inputPDF, String outputPDF) throws Exception {
        PDDocument document = null;
        try {
            document = PDDocument.load(new File(inputPDF));
            if (document.isEncrypted()) {
                throw new Exception("Cannot add bookmarks to encrypted document.");
            }

            PDDocumentOutline outline = new PDDocumentOutline();
            document.getDocumentCatalog().setDocumentOutline(outline);
            outline.addLast(bookmarkRoot);
            bookmarkRoot.openNode();
            outline.openNode();

            document.save(outputPDF);
        } finally {
            if (document != null) {
                document.close();
            }
        }
    }

    /**
     * 生成书签文件
     * 
     * @param inputPDF
     *            待生成书签的PDF文件路径
     * @param bookmarkDemoFile
     *            书签样例文件路径
     * @param outputPDF
     *            添加书签后的PDF文件路径
     */
    public static void makeBookmark(String inputPDF, String bookmarkDemoFile, String outputPDF) {
        AutoBookmarkMaker maker = new AutoBookmarkMaker();
        try {
            maker.loadBookmarkPattern(inputPDF, bookmarkDemoFile);
            maker.loadBookmark(inputPDF);
            maker.createBookmark(inputPDF, outputPDF);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成书签示例，以供修改
     * 
     */
    public static void makeBookmarkDemoFile(String outputFile) {
        /**
         * 书签示例列表，ArrayList<String>存放某一级书签的示例，而ArrayList<ArrayList<String>>则存入各级书签的示例
         */
        ArrayList<ArrayList<String>> bookmarkDemoList = new ArrayList<ArrayList<String>>();
        ArrayList<String> itemDemoList = null;
        /* level 1 */
        itemDemoList = new ArrayList<String>();
        itemDemoList.add("chapter 1");
        itemDemoList.add("chapter 2");
        bookmarkDemoList.add(itemDemoList);
        /* level 2 */
        itemDemoList = new ArrayList<String>();
        itemDemoList.add("chapter 1.1");
        itemDemoList.add("chapter 1.2");
        bookmarkDemoList.add(itemDemoList);

        Writer writer = null;

        try {
            FileOutputStream outputStream = new FileOutputStream(outputFile != null ? outputFile : "bk_demo.json");
            writer = new OutputStreamWriter(outputStream, "UTF-8");
            Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
            gson.toJson(bookmarkDemoList, writer);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
